<!DOCTYPE html>
<html>
<head>
<title>DAY06</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style type="text/css">
/* GitHub stylesheet for MarkdownPad (http://markdownpad.com) */
/* Author: Nicolas Hery - http://nicolashery.com */
/* Version: b13fe65ca28d2e568c6ed5d7f06581183df8f2ff */
/* Source: https://github.com/nicolahery/markdownpad-github */

/* RESET
=============================================================================*/

html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video {
  margin: 0;
  padding: 0;
  border: 0;
}

/* BODY
=============================================================================*/

body {
  font-family: Helvetica, arial, freesans, clean, sans-serif;
  font-size: 14px;
  line-height: 1.6;
  color: #333;
  background-color: #fff;
  padding: 20px;
  max-width: 960px;
  margin: 0 auto;
}

body>*:first-child {
  margin-top: 0 !important;
}

body>*:last-child {
  margin-bottom: 0 !important;
}

/* BLOCKS
=============================================================================*/

p, blockquote, ul, ol, dl, table, pre {
  margin: 15px 0;
}

/* HEADERS
=============================================================================*/

h1, h2, h3, h4, h5, h6 {
  margin: 20px 0 10px;
  padding: 0;
  font-weight: bold;
  -webkit-font-smoothing: antialiased;
}

h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code, h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
  font-size: inherit;
}

h1 {
  font-size: 28px;
  color: #000;
}

h2 {
  font-size: 24px;
  border-bottom: 1px solid #ccc;
  color: #000;
}

h3 {
  font-size: 18px;
}

h4 {
  font-size: 16px;
}

h5 {
  font-size: 14px;
}

h6 {
  color: #777;
  font-size: 14px;
}

body>h2:first-child, body>h1:first-child, body>h1:first-child+h2, body>h3:first-child, body>h4:first-child, body>h5:first-child, body>h6:first-child {
  margin-top: 0;
  padding-top: 0;
}

a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 {
  margin-top: 0;
  padding-top: 0;
}

h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
  margin-top: 10px;
}

/* LINKS
=============================================================================*/

a {
  color: #4183C4;
  text-decoration: none;
}

a:hover {
  text-decoration: underline;
}

/* LISTS
=============================================================================*/

ul, ol {
  padding-left: 30px;
}

ul li > :first-child, 
ol li > :first-child, 
ul li ul:first-of-type, 
ol li ol:first-of-type, 
ul li ol:first-of-type, 
ol li ul:first-of-type {
  margin-top: 0px;
}

ul ul, ul ol, ol ol, ol ul {
  margin-bottom: 0;
}

dl {
  padding: 0;
}

dl dt {
  font-size: 14px;
  font-weight: bold;
  font-style: italic;
  padding: 0;
  margin: 15px 0 5px;
}

dl dt:first-child {
  padding: 0;
}

dl dt>:first-child {
  margin-top: 0px;
}

dl dt>:last-child {
  margin-bottom: 0px;
}

dl dd {
  margin: 0 0 15px;
  padding: 0 15px;
}

dl dd>:first-child {
  margin-top: 0px;
}

dl dd>:last-child {
  margin-bottom: 0px;
}

/* CODE
=============================================================================*/

pre, code, tt {
  font-size: 12px;
  font-family: Consolas, "Liberation Mono", Courier, monospace;
}

code, tt {
  margin: 0 0px;
  padding: 0px 0px;
  white-space: nowrap;
  border: 1px solid #eaeaea;
  background-color: #f8f8f8;
  border-radius: 3px;
}

pre>code {
  margin: 0;
  padding: 0;
  white-space: pre;
  border: none;
  background: transparent;
}

pre {
  background-color: #f8f8f8;
  border: 1px solid #ccc;
  font-size: 13px;
  line-height: 19px;
  overflow: auto;
  padding: 6px 10px;
  border-radius: 3px;
}

pre code, pre tt {
  background-color: transparent;
  border: none;
}

kbd {
    -moz-border-bottom-colors: none;
    -moz-border-left-colors: none;
    -moz-border-right-colors: none;
    -moz-border-top-colors: none;
    background-color: #DDDDDD;
    background-image: linear-gradient(#F1F1F1, #DDDDDD);
    background-repeat: repeat-x;
    border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
    border-image: none;
    border-radius: 2px 2px 2px 2px;
    border-style: solid;
    border-width: 1px;
    font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
    line-height: 10px;
    padding: 1px 4px;
}

/* QUOTES
=============================================================================*/

blockquote {
  border-left: 4px solid #DDD;
  padding: 0 15px;
  color: #777;
}

blockquote>:first-child {
  margin-top: 0px;
}

blockquote>:last-child {
  margin-bottom: 0px;
}

/* HORIZONTAL RULES
=============================================================================*/

hr {
  clear: both;
  margin: 15px 0;
  height: 0px;
  overflow: hidden;
  border: none;
  background: transparent;
  border-bottom: 4px solid #ddd;
  padding: 0;
}

/* TABLES
=============================================================================*/

table th {
  font-weight: bold;
}

table th, table td {
  border: 1px solid #ccc;
  padding: 6px 13px;
}

table tr {
  border-top: 1px solid #ccc;
  background-color: #fff;
}

table tr:nth-child(2n) {
  background-color: #f8f8f8;
}

/* IMAGES
=============================================================================*/

img {
  max-width: 100%
}
</style>
</head>
<body>
<h3>20. 用户-上传头像-持久层</h3>
<p><strong>a. 规划SQL语句</strong></p>
<p>用户上传头像时，服务器端需要处理的任务有2个，第1是将文件保存到指定的位置，第2是将文件保存的路径存储到数据表中。</p>
<p>在数据表相关的操作中，本质上就是修改表中的字段即可：</p>
<pre><code>update t_user set avatar=?, modified_user=?, modified_time=? where uid=?
</code></pre>

<p>在执行修改之前，还应该检查用户数据是否正常，此前已经完成，无需再次开发。</p>
<p><strong>b. 接口与抽象方法</strong></p>
<p>在<code>UserMapper</code>接口中添加抽象方法：</p>
<pre><code>/**
 * 更新用户的头像
 * @param uid 用户的id
 * @param avatar 头像的路径
 * @param modifiedUser 修改执行人
 * @param modifiedTime 修改时间
 * @return 受影响的行数
 */
Integer updateAvatar(
        @Param(&quot;uid&quot;) Integer uid, 
        @Param(&quot;avatar&quot;) String avatar,
        @Param(&quot;modifiedUser&quot;) String modifiedUser,
        @Param(&quot;modifiedTime&quot;) Date modifiedTime);
</code></pre>

<p><strong>c. 配置映射</strong></p>
<p>在<code>UserMapper.xml</code>中配置以上方法的映射：</p>
<pre><code>&lt;!-- 更新用户的头像 --&gt;
&lt;!-- Integer updateAvatar(
        @Param(&quot;uid&quot;) Integer uid, 
        @Param(&quot;avatar&quot;) String avatar,
        @Param(&quot;modifiedUser&quot;) String modifiedUser,
        @Param(&quot;modifiedTime&quot;) Date modifiedTime); --&gt;
&lt;update id=&quot;updateAvatar&quot;&gt;
    UPDATE 
        t_user 
    SET 
        avatar=#{avatar}, 
        modified_user=#{modifiedUser}, 
        modified_time=#{modifiedTime} 
    WHERE 
        uid=#{uid}
&lt;/update&gt;
</code></pre>

<p>在<code>UserMapperTests</code>中编写并执行单元测试：</p>
<pre><code>@Test
public void updateAvatar() {
    Integer uid = 10;
    String avatar = &quot;这是一个头像路径&quot;;
    String modifiedUser = &quot;超级管理员&quot;;
    Date modifiedTime = new Date();
    Integer rows = mapper.updateAvatar(uid, avatar, modifiedUser, modifiedTime);
    System.err.println(&quot;rows=&quot; + rows);
}
</code></pre>

<h3>21. 用户-上传头像-业务层</h3>
<p><strong>a. 规划异常</strong></p>
<p>对于更新数据而言，必然可能出现<code>UpdateException</code>；</p>
<p>在更新之前，还需要检查用户数据，则可能出现<code>UserNotFoundException</code>。</p>
<p>以上异常类均已经存在，无需创建新的异常类。</p>
<p><strong>b. 接口与抽象方法</strong></p>
<p>在<code>IUserService</code>中添加抽象方法：</p>
<pre><code>/**
 * 修改头像
 * @param uid 用户的id
 * @param username 用户名
 * @param avatar 头像的路径
 * @throws UserNotFoundException 用户数据不存在，或者已经被标记为删除
 * @throws UpdateException 更新数据失败
 */
void changeAvatar(Integer uid, String username, String avatar) 
    throws UserNotFoundException, UpdateException;
</code></pre>

<p><strong>c. 实现类与重写方法</strong></p>
<p>在<code>UserServiceImpl</code>中重写以上方法：</p>
<pre><code>public void changeAvatar(
    Integer uid, String username, String avatar) 
        throws UserNotFoundException, UpdateException {
    // 根据参数uid查询用户数据
    // 判断查询结果是否为null
    // 抛出：UserNotFoundException

    // 判断查询结果中的isDelete为1
    // 抛出：UserNotFoundException

    // 创建当前时间对象
    // 执行更新头像，并获取返回的受影响的行数
    // 判断受影响的行数是否不为1
    // 抛出：UpdateException
}
</code></pre>

<p>具体实现为：</p>
<pre><code>@Override
public void changeAvatar(Integer uid, String username, String avatar)
        throws UserNotFoundException, UpdateException {
    // 根据参数uid查询用户数据
    User result = userMapper.findByUid(uid);
    // 判断查询结果是否为null
    if (result == null) {
        // 抛出：UserNotFoundException
        throw new UserNotFoundException(
            &quot;修改头像失败！用户名不存在！&quot;);
    }

    // 判断查询结果中的isDelete为1
    if (result.getIsDelete() == 1) {
        // 抛出：UserNotFoundException
        throw new UserNotFoundException(
            &quot;修改头像失败！用户名不存在！&quot;);
    }

    // 创建当前时间对象
    Date now = new Date();
    // 执行更新头像，并获取返回的受影响的行数
    Integer rows = userMapper.updateAvatar(uid, avatar, username, now);
    // 判断受影响的行数是否不为1
    if (rows != 1) {
        // 抛出：UpdateException
        throw new UpdateException(
            &quot;修改头像失败！更新头像时出现未知错误！&quot;);
    }
}
</code></pre>

<p>在<code>UserServiceTests</code>中编写并执行单元测试：</p>
<pre><code>@Test
public void changeAvatar() {
    try {
        Integer uid = 1000;
        String username = &quot;系统管理员&quot;;
        String avatar = &quot;这是一个新头像的路径&quot;;
        service.changeAvatar(uid, username, avatar);
        System.err.println(&quot;OK.&quot;);
    } catch (ServiceException e) {
        System.err.println(e.getClass().getName());
        System.err.println(e.getMessage());
    }
}
</code></pre>

<h3>22. 用户-上传头像-控制器层</h3>
<p><strong>a. 处理异常</strong></p>
<p>此次用户上传头像时，可能会出现以下问题：未选择上传的文件或文件大小为0字节；文件大小超出限制；文件类型超出允许的范围。</p>
<p>在上传过程中，存储文件时，调用的<code>transferTo()</code>方法会抛出2种异常：</p>
<pre><code>try {
    file.transferTo(dest);
} catch (IllegalStateException e) {
    e.printStackTrace();
    throw new FileUploadStateException(&quot;...&quot;);
} catch (IOException e) {
    e.printStackTrace();
    throw new FileUploadIOException(&quot;...&quot;);
}
</code></pre>

<p>此时，不能直接将这2种异常再次抛出，因为，抛出的异常都会被方法的调用者处理，而当前控制器方法是会被SpringMVC框架调用的，框架本身并不会对这2种异常进行处理，所以，一旦出现异常，就会在页面中显示500错误，是极为不合适的！</p>
<p>针对这样的异常，应该在捕获这2个异常的时候，再次抛出自定义异常，并且在<code>BaseController</code>中处理自定义异常！</p>
<p>则应该创建异常类：</p>
<pre><code>cn.tedu.store.controller.ex.FileUploadException 继承自 RuntimeException

cn.tedu.store.controller.ex.FileEmptyException 继承自 FileUploadException
cn.tedu.store.controller.ex.FileSizeException 继承自 FileUploadException
cn.tedu.store.controller.ex.FileTypeException 继承自 FileUploadException

cn.tedu.store.controller.ex.FileUploadIOException 继承自 FileUploadException
cn.tedu.store.controller.ex.FileUploadStateException 继承自 FileUploadException
</code></pre>

<p>创建异常后，还应该在<code>BaseController</code>统一处理异常，首先需要调整处理哪些异常：</p>
<pre><code>@ExceptionHandler({ServiceException.class, FileUploadException.class})
</code></pre>

<p><strong>b. 设计请求</strong></p>
<pre><code>请求路径：/users/change_avatar
请求参数：HttpServletRequest request, MultipartFile file
请求类型：POST
响应数据：JsonResult&lt;String&gt;
</code></pre>

<p><strong>c. 处理请求</strong></p>
<pre><code>public static final int AVATAR_MAX_SIZE = 1 * 1024 * 1024;

public static final List&lt;String&gt; AVATAR_CONTENT_TYPES = new ArrayList&lt;&gt;();

public static final String AVATAR_DIR = &quot;upload&quot;;

static {
    AVATAR_CONTENT_TYPES.add(&quot;image/jpeg&quot;);
    AVATAR_CONTENT_TYPES.add(&quot;image/png&quot;);
}

@PostMapping(&quot;change_avatar&quot;)
public JsonResult&lt;String&gt; changeAvatar(
    HttpServletRequest request, 
    @RequestParam(&quot;file&quot;) MultipartFile file) {
    // 检查文件是否为空
    if (file.isEmpty()) {
        throw new FileEmptyException(&quot;....&quot;);
    }

    // 检查文件大小
    if (file.getSize() &gt; AVATAR_MAX_SIZE) {
        throw new FileSizeException(&quot;....&quot;);
    }

    // 检查文件类型
    if (!AVATAR_CONTENT_TYPES.contains(file.getContentType())) {
        throw new FileTypeException(&quot;....&quot;);
    }

    // 确定文件夹
    String dirPath = request.getServletContext().getRealPath(AVATAR_DIR);
    File dir = new File(dirPath);
    if (!dir.exists()) {
        dir.mkdirs();
    }

    // 确定文件名
    String originalFilename = file.getOriginalFilename();
    String suffix = &quot;&quot;;
    int beginIndex = originalFilename.lastIndexOf(&quot;.&quot;);
    if (beginIndex != -1) {
        suffix = originalFilename.substring(beginIndex);
    }
    String filename = UUID.randomUUID().toString() + suffix;

    // 执行保存
    File dest = new File(dir, filename);
    file.transferTo(dest);

    // 更新数据表
    String avatar = &quot;/&quot; + AVATAR_DIR + &quot;/&quot; + filename;
    HttpSession session = request.getSession();
    Integer uid = ...
    String username = ...
    userService.changeAvatar(uid, username, avatar);

    // 返回
    JsonResult&lt;String&gt; jr = new JsonResult&lt;String&gt;();
    jr.setState(SUCCESS);
    jr.setData(avatar);
    return jr;
}
</code></pre>

<h3>23. 用户-上传头像-前端页面</h3>
<p>当使用jQuery的<code>$.ajax()</code>函数实现文件上传时，与普通的AJAX请求有2点区别：</p>
<ol>
<li>
<p>获取数据的方式：普通的请求可以通过<code>$(&quot;#form-id&quot;).serialize()</code>，而上传时需要使用<code>new FormData($(&quot;#form-id&quot;)[0])</code>；</p>
</li>
<li>
<p>必须添加2项新的属性配置：<code>&quot;contentType&quot;:false</code>和<code>&quot;processData&quot;:false</code></p>
</li>
</ol>
<h3>24. 收货地址-分析</h3>
<p>关于收货地址的管理，涉及的功能有：增加，查看列表，修改，删除，设为默认。</p>
<p>以上功能的开发顺序应该是：增加 &gt; 查看列表 &gt; 设为默认 &gt; 删除 &gt; 修改。</p>
<h3>25. 收货地址-创建数据表</h3>
<pre><code>CREATE TABLE t_address (
    aid INT AUTO_INCREMENT COMMENT '收货地址id',
    uid INT COMMENT '用户id',
    name VARCHAR(50) COMMENT '收货人姓名',
    province_code CHAR(6) COMMENT '省-代号',
    province_name VARCHAR(50) COMMENT '省-名称',
    city_code CHAR(6) COMMENT '市-代号',
    city_name VARCHAR(50) COMMENT '市-名称',
    area_code CHAR(6) COMMENT '区-代号',
    area_name VARCHAR(50) COMMENT '区-名称',
    zip CHAR(6) COMMENT '邮编',
    address VARCHAR(100) COMMENT '详细地址',
    phone VARCHAR(20) COMMENT '手机',
    tel VARCHAR(20) COMMENT '固话',
    tag VARCHAR(20) COMMENT '地址类型',
    is_default INT COMMENT '是否默认：0-非默认，1-默认',
    created_user VARCHAR(20) COMMENT '创建人',
    created_time DATETIME COMMENT '创建时间',
    modified_user VARCHAR(20) COMMENT '最后修改人',
    modified_time DATETIME COMMENT '最后修改时间',
    PRIMARY KEY (aid)
) DEFAULT CHARSET=UTF8;
</code></pre>

<h3>26. 收货地址-创建实体类</h3>
<p>创建<code>cn.tedu.store.entity.Address</code>继承自<code>BaseEntity</code>。</p>
<h3>27. 收货地址-增加-持久层</h3>
<p><strong>a. 规划SQL语句</strong></p>
<p>增加收货地址的SQL语句应该是：</p>
<pre><code>insert into t_address (除了aid以外的所有字段) values(匹配的实体类的属性值);
</code></pre>

<p>在增加时，还需要考虑当前增加的收货地址是不是当前用户的默认收货地址，可以设定规则“用户的第1条收货地址是默认的，后续增加的都不是默认的”，则可以查询该用户的收货地址数量，如果为0，则即将增加的就是第1条，就应该是默认的，如果不为0，表示用户已经有一些收货地址数据了，即将增加的不是第1条，则应该是非默认的！所以，需要“查询该用户的收货地址数量”的查询功能，对应的SQL语句应该是：</p>
<pre><code>select count(*) from t_address where uid=?
</code></pre>

<p><strong>b. 接口与抽象方法</strong></p>
<p>创建<code>cn.tedu.store.mapper.AddressMapper</code>接口，并添加抽象方法：</p>
<pre><code>Integer insert(Address address);

Integer countByUid(Integer uid);
</code></pre>

<p><strong>c. 配置映射</strong></p>
<p>复制得到<code>AddressMapper.xml</code>文件，确定根节点的<code>namespace</code>对应以上接口，然后，配置以上2个抽象方法的映射：</p>
<p>在<strong>src/test/java</strong>下创建<code>AddressMapperTests</code>单元测试类，添加测试相关注解，然后，编写并执行以上2个功能的单元测试：</p>
<h3>28. 收货地址-增加-业务层</h3>
<p><strong>a. 规划异常</strong></p>
<p><strong>b. 接口与抽象方法</strong></p>
<p><strong>c. 实现类与重写方法</strong></p>
<h3>29. 收货地址-增加-控制器层</h3>
<p><strong>a. 处理异常</strong></p>
<p><strong>b. 设计请求</strong></p>
<p><strong>c. 处理请求</strong></p>
<h3>30. 收货地址-增加-前端页面</h3>
<h3>------------------------------------------------------</h3>
<h3>设置SpringBoot项目上传限制</h3>
<p>在启动类之前添加<code>@Configuration</code>注解，并在类中添加：</p>
<pre><code>@Bean
public MultipartConfigElement getMultipartConfig() {
    MultipartConfigFactory factory
        = new MultipartConfigFactory();

    DataSize maxFileSize = DataSize.ofMegabytes(50);
    factory.setMaxFileSize(maxFileSize);
    DataSize maxRequestSize = DataSize.ofMegabytes(100);
    factory.setMaxRequestSize(maxRequestSize);

    return factory.createMultipartConfig();
}
</code></pre>


</body>
</html>
<!-- This document was created with MarkdownPad, the Markdown editor for Windows (http://markdownpad.com) -->
